using System;
using System.Linq;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Collections.Generic;


namespace FoundationLib.Common.Calendar
{
	/// <summary>
	/// This class represents a week of a calendar year.
	/// </summary>
	public class WeekOfYear : IComparable<WeekOfYear>
	{
		/// <summary>
		/// The week number with regard to the beginning of the year
		/// </summary>
		private int week;
		/// <summary>
		/// The year
		/// </summary>
		private int year;
		/// <summary>
		/// The cached start date of this week of year
		/// </summary>
		private DateTime? startDate;
		/// <summary>
		/// The culture information used internally
		/// </summary>
		private CultureInfo culture;
		/// <summary>
		/// A set of days in this week
		/// </summary>
		private List<DateTime> days;
		/// <summary>
		/// The last-used instance formatter
		/// </summary>
		private WeekOfYearFormatter formatter;


		/// <summary>
		/// Creates a new week of year instance.
		/// </summary>
		/// <param name="week">The week number</param>
		/// <param name="year">The calendar year</param>
		public WeekOfYear(int week, int year) : this()
		{
			Contract.Requires(week > 0 && week < 55 && year > 1900);

			int weeksInYear = (int)Math.Round(this.culture.Calendar.GetDaysInYear(year) / 7.0);
			if (week > weeksInYear)
				throw new ArgumentException("The given week number is greater than the maximum number of weeks in the given year.", "week");

			this.week = week;
			this.year = year;
		}

		/// <summary>
		/// The class invariant.
		/// </summary>
		[ContractInvariantMethod]
		private void ObjectInvariant()
		{
			Contract.Invariant(this.culture != null);
			Contract.Invariant(this.days != null);
			Contract.Invariant(this.formatter != null);
		}

		/// <summary>
		/// Returns a week of year instance for the given date.
		/// </summary>
		/// <param name="date">The date</param>
		/// <param name="culture">The culture to be used</param>
		/// <returns>A week of year instance</returns>
		public static WeekOfYear FromDate(DateTime date, CultureInfo culture)
		{
			Contract.Requires(culture != null);
			WeekOfYear instance = new WeekOfYear();
			instance.culture = culture;
			instance.week = instance.culture.Calendar.GetWeekOfYear(date, 
				culture.DateTimeFormat.CalendarWeekRule, 
				culture.DateTimeFormat.FirstDayOfWeek);
			instance.year = date.Year;
			return instance;
		}

		/// <summary>
		/// Returns a week of year instance for the given date.
		/// </summary>
		/// <param name="date">The date</param>
		/// <returns>A week of year instance</returns>
		public static WeekOfYear FromDate(DateTime date)
		{
			return (FromDate(date, CultureInfo.CurrentCulture));
		}

		/// <summary>
		/// Creates a new week of year instance.
		/// </summary>
		private WeekOfYear()
		{
			this.culture = CultureInfo.CurrentCulture;
			this.days = new List<DateTime>();
			this.formatter = new DefaultWeekOfYearFormatter();
		}

		/// <summary>
		/// Creates a new week of year
		/// </summary>
		/// <param name="week">The week number relative to the year</param>
		/// <param name="year">The calendar year</param>
		/// <param name="culture">The culture to be used</param>
		public WeekOfYear(int week, int year, CultureInfo culture)
			: this(week, year)
		{
			Contract.Requires(culture != null);
			this.culture = culture;
		}

		/// <summary>
		/// Returns the date of Monday of the first week of the year.
		/// </summary>
		/// <returns>The date representing the first day of the first week of the year</returns>
		private DateTime GetMondayOfFirstWeek()
		{
			DateTime first = new DateTime(this.year, 1, 1);
			int offset = (int)this.culture.DateTimeFormat.FirstDayOfWeek - (int)first.DayOfWeek;
			first = first.AddDays(offset);
			Contract.Assert(first.DayOfWeek == this.culture.DateTimeFormat.FirstDayOfWeek);
			return first;
		}

		/// <summary>
		/// Gets the end date of this week of year.
		/// </summary>
		public DateTime EndDate
		{
			get
			{
				return (this.StartDate.AddDays(6.0).Date);
			}
		}

		/// <summary>
		/// Gets the start date of this week of year.
		/// </summary>
		public DateTime StartDate
		{
			get
			{
				if (!this.startDate.HasValue)
				{

					DateTime first = this.GetMondayOfFirstWeek();
					int firstWeek = this.culture.Calendar.GetWeekOfYear(first,
						this.culture.DateTimeFormat.CalendarWeekRule,
						this.culture.DateTimeFormat.FirstDayOfWeek);

					int offset = firstWeek <= 1 || firstWeek > 52 ? this.week - 1 : this.week;

					double daysToAdd = offset * 7.0;
					this.startDate = first.AddDays(daysToAdd).Date;
				}
				return this.startDate.Value;
			}
		}

		/// <summary>
		/// Gets all dates contained in this calendar week.
		/// </summary>
		public DateTime[] Days
		{
			get
			{
				if (this.days.Count() == 0)
				{
					DateTime current = this.StartDate;
					while (current <= this.EndDate)
					{
						this.days.Add(current);
						current = current.AddDays(1);
					}
				}

				return this.days.ToArray();
			}
		}

		/// <summary>
		/// Adds the specified time span to the current instance and returns the resulting
		/// week of year.
		/// </summary>
		/// <param name="value">The time span to add</param>
		/// <returns>The new week of year</returns>
		public WeekOfYear Add(TimeSpan value)
		{
			return WeekOfYear.FromDate(this.StartDate.Add(value));
		}

		/// <summary>
		/// Adds the specified number of weeks to the current instance and returns the resulting
		/// week of year.
		/// </summary>
		/// <param name="value">The number of weeks to add</param>
		/// <returns>The new week of year</returns>
		public WeekOfYear AddWeeks(int value)
		{
			return WeekOfYear.FromDate(this.StartDate.AddDays(value * 7.0));
		}

		/// <see cref="Object.Equals(object)"/>
		public override bool Equals(object obj)
		{
			if (obj == null || !(obj is WeekOfYear))
				return false;
			WeekOfYear instance = obj as WeekOfYear;
			return (instance.StartDate == this.StartDate);
		}

		/// <see cref="Object.ToString()"/>
		public override string ToString()
		{
			return (String.Format("{0}/{1}", this.week, this.year));
		}

		/// <summary>
		/// Returns a formatted string representing this week of year instance.
		/// </summary>
		/// <param name="formatter">The week of year formatter</param>
		/// <returns>A formatted string</returns>
		public string Format(WeekOfYearFormatter formatter)
		{
			Contract.Requires(formatter != null);
			this.formatter = formatter;
			return formatter.Format(this);
		}

		/// <summary>
		/// Returns a formatted string representing this week of year instance.
		/// </summary>
		/// <returns>A formatted string</returns>
		public string Format()
		{
			return (this.Format(this.formatter));
		}

		/// <summary>
		/// Tests two <see cref="WeekOfYear"/> instances on equality.
		/// </summary>
		/// <param name="first">The first instance</param>
		/// <param name="second">The second instance</param>
		/// <returns>True if both instances are equal, false otherwise</returns>
		public static bool operator ==(WeekOfYear first, WeekOfYear second)
		{
			return first.Equals(second);
		}

		/// <summary>
		/// Tests whether the first <see cref="WeekOfYear"/> instance is greater than the second.
		/// </summary>
		/// <param name="first">The first instance</param>
		/// <param name="second">The second instance</param>
		/// <returns>True if <paramref name="first"/> is greater than <paramref name="second"/>, false otherwise</returns>
		public static bool operator >(WeekOfYear first, WeekOfYear second)
		{
			return (first.CompareTo(second) > 0);
		}

		/// <summary>
		/// Tests whether the first <see cref="WeekOfYear"/> instance is less than the second.
		/// </summary>
		/// <param name="first">The first instance</param>
		/// <param name="second">The second instance</param>
		/// <returns>True if <paramref name="first"/> is less than <paramref name="second"/>, false otherwise</returns>
		public static bool operator <(WeekOfYear first, WeekOfYear second)
		{
			return (first.CompareTo(second) < 0);
		}

		/// <summary>
		/// Tests whether the first <see cref="WeekOfYear"/> instance is greater or equal to the second.
		/// </summary>
		/// <param name="first">The first instance</param>
		/// <param name="second">The second instance</param>
		/// <returns>True if <paramref name="first"/> is greater or equal to <paramref name="second"/>, false otherwise</returns>
		public static bool operator >=(WeekOfYear first, WeekOfYear second)
		{
			return (first > second || first == second);
		}

		/// <summary>
		/// Tests whether the first <see cref="WeekOfYear"/> instance is less or equal to the second.
		/// </summary>
		/// <param name="first">The first instance</param>
		/// <param name="second">The second instance</param>
		/// <returns>True if <paramref name="first"/> is less or equal to <paramref name="second"/>, false otherwise</returns>
		public static bool operator <=(WeekOfYear first, WeekOfYear second)
		{
			return (first < second || first == second);
		}

		/// <see cref="Object.GetHashCode()"/>
		public override int GetHashCode()
		{
			return this.StartDate.GetHashCode();
		}

		/// <summary>
		/// Tests two <see cref="WeekOfYear"/> instances on inequality.
		/// </summary>
		/// <param name="first">The first instance</param>
		/// <param name="second">The second instance</param>
		/// <returns>True if the instances are inequal, false otherwise</returns>
		public static bool operator !=(WeekOfYear first, WeekOfYear second)
		{
			return !first.Equals(second);
		}

		/// <summary>
		/// Subtracts two week of year instances and returns the distance in weeks.
		/// </summary>
		/// <remarks>
		/// If <paramref name="first"/> is less than <paramref name="second"/>, a negative number is returned.
		/// </remarks>
		/// <param name="first">The first instance</param>
		/// <param name="second">The second instance</param>
		/// <returns>The distance in weeks</returns>
		public static double operator -(WeekOfYear first, WeekOfYear second)
		{
			return (first.StartDate - second.StartDate).TotalDays / 7.0;
		}

		/// <summary>
		/// Gets the week number with regard to the beginning of the year.
		/// </summary>
		public int Week
		{
			get
			{
				return this.week;
			}
		}

		/// <summary>
		/// Gets the calendar year.
		/// </summary>
		public int Year
		{
			get
			{
				return this.year;
			}
		}

		/// <summary>
		/// Gets the current week of year.
		/// </summary>
		public static WeekOfYear ThisWeek
		{
			get
			{
				return WeekOfYear.FromDate(DateTime.Today);
			}
		}

		/// <summary>
		/// Gets an instance for the week before the current week of year.
		/// </summary>
		public static WeekOfYear LastWeek
		{
			get
			{
				return WeekOfYear.ThisWeek.AddWeeks(-1);
			}
		}

		/// <summary>
		/// Gets an instance representing one week after the current week of year.
		/// </summary>
		public static WeekOfYear NextWeek
		{
			get
			{
				return WeekOfYear.ThisWeek.AddWeeks(1);
			}
		}

		/// <see cref="IComparable{T}.CompareTo(T)"/>
		public int CompareTo(WeekOfYear other)
		{
			if (other == null)
				throw new ArgumentNullException("other");

			if (other.Year == this.Year)
			{
				if (other.Week == this.Week)
				{
					return 0;
				}
				else if (other.Week < this.Week)
				{
					return 1;
				}
				else
				{
					return -1;
				}
			}
			else if (other.Year < this.Year)
			{
				return 1;
			}
			else
			{
				return -1;
			}
		}
	}

}